# -*- coding: utf-8 -*-
#'django-serializable'
#Copyright (C) 2010 Sébastien Piquemal @ futurice
#contact : sebastien.piquemal@futurice.com
#futurice's website : www.futurice.com

#This program is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.

#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.

#You should have received a copy of the GNU General Public License
#along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
.. warning::
        This doesn't work with the version 1.1 of Django. The version 1.2 must be used because the serializers doesn't have the features we require in django 1.1 (especially the `use_natural_keys` feature).
"""

import pickle

from django.db.models import ForeignKey, CharField
from django.db.models.fields import FieldDoesNotExist
from django.core import serializers
from django.db import models

class SerializableManagerMixin(object):
    """
    Mixin for using with your custom managers, in case you don't want to use :class:`SerializableManager` as a default manager for your models. Example of use : ::
        
        class MyManager(Manager, SerializableManagerMixin):
            ...
    """

    def get_by_natural_key(self, *values):
        """
        This method fetches and return an object by its natural key.

        Each value passed as a parameter is mapped with a field name, according to the blueprint :attr:`nk_field_name <Serializable.nk_field_names>` of the model that is managed. For example, if : ::
            
            nk_field_names = ("firstname", "lastname")

        a good use of this method is : ::

            AManagedClass.manager_instance.get_by_natural_key("Michael", "Moore")

        :raises: :class:`DeserializationError` if the natural key provided doesn't have enough values or has too much, to be mapped with :attr:`nk_field_names`
        """
        #We test if nk_field_names is a tuple to avoid weird error messages
        if not type(self.model.nk_field_names) in [tuple, list]:
            raise TypeError, "nk_field_names should be of type %s or %s, found : %s"%(tuple, list, type(self.nk_field_names))

        names = self.model.nk_field_names
        if not len(values) == len(names):
            raise DeserializationError, "Invalid natural key : %s. The fields expected are : %s"%(values, names)

        #This dictionnary will be passed to the 'get' method in order to lookup for an object by its natural key
        lookup_dict = dict()
        #We call get_by_natural_key on the natural_keys that we have, if they contain tuples.
        for name, value in zip(names, values):
            try :
                field = self.model._meta.get_field(name)
            except FieldDoesNotExist :
                pass
            #if it's a field, let's resolve recursively the value given as a natural key
            else :
                if isinstance(field, ForeignKey) and\
                        hasattr(field.related.parent_model.objects, "get_by_natural_key"):
                    if hasattr(field.related.parent_model, "nk_field_names"):
                        #in this case value must be unpacked
                        if len(field.related.parent_model.nk_field_names) > 1:
                            value = field.related.parent_model.objects.get_by_natural_key(*value)
                        else :
                            value = field.related.parent_model.objects.get_by_natural_key(value)
                    else :
                        raise DeserializationError("Could not find 'nk_field_names' for %s" % field)
            lookup_dict[name] = value
        try :
            found_object = self.get(**lookup_dict)
        except self.model.DoesNotExist :
            raise self.model.DoesNotExist, "There is no row of %s, with the natural key : %s "%(self.model, lookup_dict)
        except self.model.MultipleObjectsReturned :
            raise self.model.MultipleObjectsReturned, "There are multiple rows of %s, with the natural key : %s "%(self.model, lookup_dict)
        return found_object


class SerializableMixin(object):
    """
    Mixin for using with your models, in case you don't want to use :class:`Serializable` as a base class for your models. Example of use : ::

        class MyModel(Model, SerializableMixin):
            ...
    """

    serialize_fields = []
    """This class attribute is a list of :class:`str` which are the names of fields/properties that should be serialized when serializing an instance."""

    deserialize_fields = []
    """This class attribute is a list of :class:`str` which are the names of fields/properties that should be deserialized when deserializing data into an instance."""

    nk_field_names = ("pk",)
    """This class attribute is a tuple of :class:`str`. It is the 'blueprint' of natural keys for a class. This means that the values in every tuple that is considered to be the natural key of an instance of this class, map to the field names in this blueprint. Reciprocally, to generate the natural key for any instance, this blueprint is used. See :ref:`some examples<serialization_examples>`

    .. attention :: If this tuple contains only one element, the natural key generated will be a simple :class:`str` instead of a :class:`tuple`.
"""

    def serialize(self):
        """
        Returns a dictionnary containing only the calling object's fields and properties that are referenced in the class attribute :attr:`serialize_fields`. The returned dictionnary has the following format *{field_name : field_value}*. The foreign keys are replaced by natural keys generated according to :attr:`nk_field_names`.
        
        :raise: :class:`SerializationError` if trying to serialize an field/property that doesn't exist.
        """
        #Serialize fields
        all_serialized = serializers.serialize("python", [self], fields=self.serialize_fields, use_natural_keys=True)[0]["fields"]
        #Serialize properties
        attrs_left = set(self.serialize_fields) - set(all_serialized)
        for name in attrs_left :
            if hasattr(self, name):
                all_serialized[name] = getattr(self, name)
            else :
                raise SerializationError("The attribute %s doesn't exist" % name)
        return all_serialized
        
    
    @classmethod
    def deserialize(cls, data):
        """
        Fetches, in the parameter `data`, only the fields/properties that are also referenced in the class attribute :attr:`deserialize_fields`, and returns a new object (not yet saved in the database) with the fields fetched. The foreign keys must be entered as natural keys, they will be matched to actual objects according to the :attr:`nk_field_names` of the foreign classes.

        :param data: *{field_name : field_value}*

        :raises: :class:`DeserializationError` in three different cases :

            - if a natural key provided in `data` doesn't have enough values or has too much, to be mapped with :attr:`nk_field_names`
            - if a natural key yielded multiple results
            - if a natural key yielded no result
            - trying to deserialize a field/property that hasn't been initialized, or doesn't exist
        
        .. todo:: ugly implementation because the behaviour of the Django serializers is not well documented
        .. todo:: if somebody has the 'good' idea to explicitely define its 'pk' (e.g, as 'object_id'), and to put both this field AND 'pk', imprevisible behaviour... because we mix-up fields and properties.
        """
        #---- Dictionnaries of values to deserialize
        #fields that are both in the 'data' parameter and 'cls.deserialize_fields'
        attr_names = set(data.keys()) & set(cls.deserialize_fields)
        #{<field_name>: <field_value>} to deserialize
        fields = dict()
        #{<property_name>: <property_value>} to deserialize
        properties = dict()
        for name in attr_names :
            # the serializers want tuples for the keys, so if we have a natural key that is only a str it will fail.
            # however, when the key is not a natural key, the serializer doesn't want a tuple...
            try :
                if isinstance(cls._meta.get_field(name), ForeignKey) and\
                        not type(data[name]) in [tuple, list] and \
                        issubclass(cls._meta.get_field(name).related.parent_model, Serializable) :
                    fields[name] = (data[name],)
                else :
                    fields[name] = data[name]
            except FieldDoesNotExist:
                properties[name] = data[name]
        #---- Deserialize fields, using django serializers
        serialized_obj = {"pk": 0,
                            "model": "%s.%s"%(cls._meta.app_label, cls._meta.module_name),
                            "fields": fields}
        #A generator is returned by 'deserialize', so we convert it to a list and take the first element .object
        try :
            deserialized_obj = list(serializers.deserialize("python", [serialized_obj]))[0].object
        except cls.DoesNotExist as e :
            raise DeserializationError(e)
        except cls.MultipleObjectsReturned as e :
            raise DeserializationError(e)

        #---- Deserialize the properties
        for name, value in properties.iteritems():
            if hasattr(deserialized_obj, name):
                setattr(deserialized_obj, name, value)
            else :
                raise DeserializationError("The attribute %s is not initialized" % name)
        #A trick, so the 'pk' is auto-attributed (when saving) if not explicitely given in 'deserialize_fields'.
        if not 'pk' in properties.keys() :
            deserialized_obj.pk = None
        return deserialized_obj

    def update (self, data) :
        """
        Updates the calling object with the data passed as a parameter. This method works exactly as :meth:`Serializable.deserialize`, except that no new object is created. The changes introduced on the calling object are not yet saved in the database.
        """
        buffer_obj = self.__class__.deserialize(data)
        #We keep only the fields that are both in the 'data' parameter and the variable 'deserialize_fields'
        attr_names = set(data.keys()) & set(self.deserialize_fields)
        for name in attr_names :
            setattr(self, name, getattr(buffer_obj, name))

    def natural_key(self) :
        """
        This method returns what is considered to be the *natural key* of the calling object.

        There is no need to override it, as it uses the attribute :attr:`nk_field_names` as a blueprint for automatic `natural key` generation.
        """
        #We test if nk_field_names is a tuple to avoid weird error messages
        if not isinstance(self.nk_field_names, tuple) :
            raise TypeError, "nk_field_names should be of type %s, found : %s"%(tuple, type(self.nk_field_names))
        #Call recursively 'natural_key' on related objects, if they have a natural_key
        nk = []
        for field_name in self.nk_field_names:
            value = getattr(self, field_name)
            if hasattr(value, "natural_key"):
                value = value.natural_key()
            nk.append(value)
        nk = tuple(nk)
        #build a tuple if more than one value only
        if len(nk) == 1 : nk = nk[0]

        return nk

class SerializableManager(models.Manager, SerializableManagerMixin):
    """
    This manager should be the default manager of any subclass of :class:`Serializable` or :class:`SerializableMixin`.
    """
    pass

class Serializable(models.Model, SerializableMixin):
    """
    A base class for adding serialization support to models. This is an abstract class, and should be subclassed for use. Subclassing only implies overriding the attributes :attr:`serialize_fields`, :attr:`deserialize_fields` and :attr:`nk_field_names`  
    """
    objects = SerializableManager()

    class Meta:
        abstract = True

class DeserializationError (Exception) :
    """
    This error is thrown in case a deserialization operation failed.
    """
    pass

class SerializationError (Exception) :
    """
    This error is thrown in case a serialization operation failed.
    """
    pass
